/* 
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using Lucene.Net.Support;

namespace Lucene.Net.Search
{
    
    /// <summary> Filter caching singleton.  It can be used 
    /// to save filters locally for reuse.
    /// This class makes it possble to cache Filters even when using RMI, as it
    /// keeps the cache on the seaercher side of the RMI connection.
    /// 
    /// Also could be used as a persistent storage for any filter as long as the
    /// filter provides a proper hashCode(), as that is used as the key in the cache.
    /// 
    /// The cache is periodically cleaned up from a separate thread to ensure the
    /// cache doesn't exceed the maximum size.
    /// </summary>
    public class FilterManager
    {
        
        protected internal static FilterManager manager;
        
        /// <summary>The default maximum number of Filters in the cache </summary>
        protected internal const int DEFAULT_CACHE_CLEAN_SIZE = 100;
        /// <summary>The default frequency of cache clenup </summary>
        protected internal const long DEFAULT_CACHE_SLEEP_TIME = 1000 * 60 * 10;
        
        /// <summary>The cache itself </summary>
        protected internal IDictionary<int, FilterItem> cache;
        /// <summary>Maximum allowed cache size </summary>
        protected internal int cacheCleanSize;
        /// <summary>Cache cleaning frequency </summary>
        protected internal long cleanSleepTime;
        /// <summary>Cache cleaner that runs in a separate thread </summary>
        protected internal FilterCleaner internalFilterCleaner;

        private static readonly object _staticSyncObj = new object();
        public static FilterManager Instance
        {
            get
            {
                lock (_staticSyncObj)
                {
                    return manager ?? (manager = new FilterManager());
                }
            }
        }

        /// <summary> Sets up the FilterManager singleton.</summary>
        protected internal FilterManager()
        {
            cache = new HashMap<int, FilterItem>();
            cacheCleanSize = DEFAULT_CACHE_CLEAN_SIZE; // Let the cache get to 100 items
            cleanSleepTime = DEFAULT_CACHE_SLEEP_TIME; // 10 minutes between cleanings
            
            internalFilterCleaner = new FilterCleaner(this);
            ThreadClass fcThread = new ThreadClass(new System.Threading.ThreadStart(internalFilterCleaner.Run));
            // setto be a Daemon so it doesn't have to be stopped
            fcThread.IsBackground = true;
            fcThread.Start();
        }

        /// <summary> Sets the max size that cache should reach before it is cleaned up</summary>
        /// <param name="value"> maximum allowed cache size </param>
        public virtual void SetCacheSize(int value)
        {
            this.cacheCleanSize = value;
        }

        /// <summary> Sets the cache cleaning frequency in milliseconds.</summary>
        /// <param name="value"> cleaning frequency in millioseconds </param>
        public virtual void SetCleanThreadSleepTime(long value)
        {
            this.cleanSleepTime = value;
        }

        /// <summary> Returns the cached version of the filter.  Allows the caller to pass up
        /// a small filter but this will keep a persistent version around and allow
        /// the caching filter to do its job.
        /// 
        /// </summary>
        /// <param name="filter">The input filter
        /// </param>
        /// <returns> The cached version of the filter
        /// </returns>
        public virtual Filter GetFilter(Filter filter)
        {
            lock (cache)
            {
                FilterItem fi = null;
                fi = cache[filter.GetHashCode()];
                if (fi != null)
                {
                    fi.timestamp = System.DateTime.UtcNow.Ticks;
                    return fi.filter;
                }
                cache[filter.GetHashCode()] = new FilterItem(filter);
                return filter;
            }
        }
        
        /// <summary> Holds the filter and the last time the filter was used, to make LRU-based
        /// cache cleaning possible.
        /// TODO: Clean this up when we switch to Java 1.5
        /// </summary>
        protected internal class FilterItem
        {
            public Filter filter;
            public long timestamp;
            
            public FilterItem(Filter filter)
            {
                this.filter = filter;
                this.timestamp = System.DateTime.UtcNow.Ticks;
            }
        }
        
        
        /// <summary> Keeps the cache from getting too big.
        /// If we were using Java 1.5, we could use LinkedHashMap and we would not need this thread
        /// to clean out the cache.
        /// 
        /// The SortedSet sortedFilterItems is used only to sort the items from the cache,
        /// so when it's time to clean up we have the TreeSet sort the FilterItems by
        /// timestamp.
        /// 
        /// Removes 1.5 * the numbers of items to make the cache smaller.
        /// For example:
        /// If cache clean size is 10, and the cache is at 15, we would remove (15 - 10) * 1.5 = 7.5 round up to 8.
        /// This way we clean the cache a bit more, and avoid having the cache cleaner having to do it frequently.
        /// </summary>
        protected internal class FilterCleaner : IThreadRunnable
        {
            private class FilterItemComparer : IComparer<KeyValuePair<int, FilterItem>>
            {
                #region IComparer<FilterItem> Members

                public int Compare(KeyValuePair<int, FilterItem> x, KeyValuePair<int, FilterItem> y)
                {
                    return x.Value.timestamp.CompareTo(y.Value.timestamp);
                }

                #endregion
            }
            
            private bool running = true;
            private FilterManager manager;
            private ISet<KeyValuePair<int, FilterItem>> sortedFilterItems;
            
            public FilterCleaner(FilterManager enclosingInstance)
            {
                this.manager = enclosingInstance;
                sortedFilterItems = new SortedSet<KeyValuePair<int, FilterItem>>(new FilterItemComparer());
            }
            
            public virtual void  Run()
            {
                while (running)
                {
                    // sort items from oldest to newest 
                    // we delete the oldest filters 
                    if (this.manager.cache.Count > this.manager.cacheCleanSize)
                    {
                        // empty the temporary set
                        sortedFilterItems.Clear();
                        lock (this.manager.cache)
                        {
                            sortedFilterItems.UnionWith(this.manager.cache);
                            int numToDelete = (int)((this.manager.cache.Count - this.manager.cacheCleanSize) * 1.5);

                            //delete all of the cache entries not used in a while
                            sortedFilterItems.ExceptWith(sortedFilterItems.Take(numToDelete).ToArray());
                        }
                        // empty the set so we don't tie up the memory
                        sortedFilterItems.Clear();
                    }
                    // take a nap
                    System.Threading.Thread.Sleep(new System.TimeSpan((System.Int64)10000 * this.manager.cleanSleepTime));
                    
                }
            }
        }
    }
}